package edu.mit.csail.pag.amock.representation;

import java.util.*;

import org.objectweb.asm.Type;

import edu.mit.csail.pag.amock.util.*;

public class TestCaseGenerator extends IndentingEmptyLineSeparatedCodeBlock
    implements ClassNameResolver {
    static private final ClassName TEST_CASE_CLASS
        = ClassName.fromDotted("edu.mit.csail.pag.amock.jmock.MockObjectTestCase");

    private String testCaseClassShort;

    private final String packageName;
    private final String testCaseName;

    private final Map<String, ClassName> importedClasses
        = new HashMap<String, ClassName>();
    private final Map<String, ClassName> importedMethods
        = new HashMap<String, ClassName>();

    private final List<TestMethodGenerator> tmgs
        = new ArrayList<TestMethodGenerator>();

    public TestCaseGenerator(String testCaseName) {
        this(testCaseName, null);
    }

    public TestCaseGenerator(String testCaseName,
                             String packageName) {
        this.testCaseName = testCaseName;
        this.packageName = packageName;
    }

    @Override public void resolveNames(ClassNameResolver cr,
                                       VariableNameBaseResolver vr) {
        // Don't call this directly!
        assert false;
    }

    private void resolveMyNames() {
        this.testCaseClassShort = this.getSourceName(TEST_CASE_CLASS);

        for (TestMethodGenerator tmg : tmgs) {
            tmg.resolveNames(this, tmg);
        }
    }

    public void printSource(LinePrinter ps) {
        resolveMyNames();
        
        printHeader(ps);

        super.printSource(ps);

        printFooter(ps);
    }

    private void printHeader(LinePrinter ps) {
        ps.line("// Generated by amock.");
        // We don't always put our generated code in a package, in
        // case it needs to refer to a class in the default package;
        // it's up to whoever makes us to decide where we should go.
        if (packageName != null) {
            ps.line("package " + packageName + ";");
        }
        ps.line("");
        printImports(ps);
        ps.line("");
        ps.line("public class " + testCaseName +
                " extends " + testCaseClassShort + " {");
    }

    private void printImports(LinePrinter ps) {
        ClassName[] longNames
            = importedClasses.values().toArray(new ClassName[0]);
        Arrays.sort(longNames);

        for (ClassName longName : longNames) {
            if (! longName.dottedPackageName().equals(this.packageName)) {
                ps.line("import " + longName.dotted() + ";");
            }
        }

        String[] methodNames = importedMethods.keySet().toArray(new String[0]);
        Arrays.sort(methodNames);

        for (String methodName : methodNames) {
            ps.line("import static "
                    + importedMethods.get(methodName).dotted() + "."
                    + methodName + ";");
        }

    }

    private void printFooter(LinePrinter ps) {
        ps.line("}");
    }

    @Override public void addChunk(CodeChunk c) {
        super.addChunk(c);
        if (c instanceof TestMethodGenerator) {
            tmgs.add((TestMethodGenerator) c);
        }
    }

    public Collection<TestMethodGenerator> getTestMethodGenerators() {
        return Collections.unmodifiableList(tmgs);
    }

    public String getSourceName(ClassName longName) {
        if (longName.isInDefaultPackage() && this.packageName != null) {
            throw new IllegalStateException("Can't use a default-package " +
                                            longName.dotted() + " in package "
                                            + this.packageName);
        }
        
        if (longName.isInDefaultPackage() || longName.isNestedClass() || longName.isBrokenXXX()) {
            // It's in the default package, or it's a nested class.
            // (Ugh!)  Just return it, without creating an import.
            return longName.dotted();
        }

        String shortName = longName.classNameWithoutPackage();

        if (importedClasses.containsKey(shortName)) {
            if (abbreviatingClassNameAs(longName, shortName)) {
                // We've already seen this class.
                return shortName;
            } else {
                // We've seen something that clashes.
                return longName.dotted();
            }
        } else {
            // We haven't seen anything like it.
            importedClasses.put(shortName, longName);
            return shortName;
        }
    }

    private boolean abbreviatingClassNameAs(ClassName longName,
                                            String shortName) {
        return importedClasses.containsKey(shortName) &&
            importedClasses.get(shortName).equals(longName);
    }

    public String getStaticMethodName(ClassName className, String methodName) {
        if (importedMethods.containsKey(methodName)) {
            if (importedMethods.get(methodName).equals(className)) {
                // We're already importing this method.
                return methodName;
            } else {
                // We're using this name for something else.  Maybe we
                // have a short class name for it at least?
                String shortName = className.classNameWithoutPackage();
                if (abbreviatingClassNameAs(className, shortName)) {
                    return shortName + "." + methodName;
                } else {
                    return className.dotted() + "." + methodName;
                }
            }
        }

        // We haven't seen anything like this yet.
        importedMethods.put(methodName, className);
        return methodName;
    }
}